const EventEmitter = require('events')
const inquirer = require('inquirer')
const { chalk, log, loadModule, isOfficialPlugin, toShortPluginId, hasYarn, hasPnpm3OrLater, resolvePkg } = require('@vue/cli-shared-utils')
const cloneDeep = require('lodash.clonedeep')

const Generator = require('./Generator')
const PackageManager = require('./util/ProjectPackageManager')

const sortObject = require('./util/sortObject')
const generateReadme = require('./util/generateReadme')
const { resolveIntroPrompts, isManualMode, writeFileTree, loadOptions, getPresets } = require('./util/index')

class Creator extends EventEmitter {
  constructor (name, context, promptModules) {
    super()

    this.name = name // aaa
    this.context = process.env.VUE_CLI_CONTEXT = context // /Users/xqd/code-2022/cli-demo/aaa
    const { presetPrompt, featurePrompt } = resolveIntroPrompts()

    this.presetPrompt = presetPrompt
    this.featurePrompt = featurePrompt
    this.outroPrompts = this.resolveOutroPrompts()
    this.injectedPrompts = []
    this.promptCompleteCbs = []
    this.afterInvokeCbs = []
    this.afterAnyInvokeCbs = []
    // this.run = this.run.bind(this)
    // const promptAPI = new PromptModuleAPI(this)
    // promptModules.forEach(m => m(promptAPI))
  }

  async create(cliOptions = {}, preset = null) { // {} null
    const { run, name, context, afterInvokeCbs, afterAnyInvokeCbs } = this

    if (!preset) {
      preset = await this.promptAndResolvePreset({ preset: 'Default (Vue 2)' })
    }

    // clone before mutating
    preset = cloneDeep(preset)
    preset.plugins['@vue/cli-service'] = Object.assign({
      projectName: name
    }, preset)

    if (cliOptions.bare) {
      preset.plugins['@vue/cli-service'].bare = true
    }

    // legacy support for router
    if (preset.router) {
      preset.plugins['@vue/cli-plugin-router'] = {}

      if (preset.routerHistoryMode) {
        preset.plugins['@vue/cli-plugin-router'].historyMode = true
      }
    }

    // legacy support for vuex
    if (preset.vuex) {
      preset.plugins['@vue/cli-plugin-vuex'] = {}
    }

    // npm | pnpm | yarn
    const packageManager = (
      cliOptions.packageManager ||
      loadOptions().packageManager ||
      (hasYarn() ? 'yarn' : null) ||
      (hasPnpm3OrLater() ? 'pnpm' : 'npm')
    )

    const pm = new PackageManager({ context, forcePackageManager: packageManager })

    log(`✨  Creating project in ${chalk.yellow(context)}.`)

    const latestMinor = '5.0.0'

    const pkg = {
      name,
      version: '0.1.0',
      private: true,
      devDependencies: {},
      ...resolvePkg(context)
    }
    const deps = Object.keys(preset.plugins)
    deps.forEach(dep => {
      if (preset.plugins[dep]._isPreset) {
        return
      }

      let { version } = preset.plugins[dep]

      if (!version) {
        if (isOfficialPlugin(dep) || dep === '@vue/cli-service' || dep === '@vue/babel-preset-env') {
          version = `~${latestMinor}`
        } else {
          version = 'latest'
        }
      }

      pkg.devDependencies[dep] = version
    })

    // write package.json  写 package.json
    await writeFileTree(context, {
      'package.json': JSON.stringify(pkg, null, 2)
    })

    // if (packageManager === 'pnpm') {

    // const shouldInitGit = this.shouldInitGit(cliOptions)

    log(`⚙\u{fe0f}  Installing CLI plugins. This might take a while...`)

    await pm.install() // 安装依赖

    log(`🚀  Invoking generators...`)
    const plugins = await this.resolvePlugins(preset.plugins, pkg) // plugins 有待执行的方法
    const generator = new Generator(context, {
      pkg,
      plugins,
      afterInvokeCbs,
      afterAnyInvokeCbs
    })
    // 赋值模板 start
    debugger
    await generator.generate({
      extractConfigFiles: preset.useConfigFiles // false
    })

    log(`📦  Installing additional dependencies...`)
    let isTestOrDebug = undefined
    if (!isTestOrDebug || process.env.VUE_CLI_TEST_DO_INSTALL_PLUGIN) {
      await pm.install() // 安装依赖
    }
    // 赋值模板 end

    // run complete cbs if any (injected by generators)
    log(`⚓  Running completion hooks...`)

    for (const cb of afterInvokeCbs) { // afterInvokeCbs
      await cb()
    }
    for (const cb of afterAnyInvokeCbs) { // afterAnyInvokeCbs: [[AsyncFunction (anonymous)]]
      await cb()
    }

    if (!generator.files['README.md']) {
      // generate README.md
      log()
      log('📄  Generating README.md...')
      await writeFileTree(context, {
        'README.md': generateReadme(generator.pkg, packageManager)
      })
    }

    // commit initial state
    let gitCommitFailed = false
    // if (shouldInitGit) {
    //   await run('git add -A')
    //   if (isTestOrDebug) {
    //     await run('git', ['config', 'user.name', 'test'])
    //     await run('git', ['config', 'user.email', 'test@test.com'])
    //     await run('git', ['config', 'commit.gpgSign', 'false'])
    //   }
    //   const msg = typeof cliOptions.git === 'string' ? cliOptions.git : 'init'
    //   try {
    //     await run('git', ['commit', '-m', msg, '--no-verify'])
    //   } catch (e) {
    //     gitCommitFailed = true
    //   }
    // }

    // log instructions
    log(`🎉  Successfully created project ${chalk.yellow(name)}.`)
    if (!cliOptions.skipGetStarted) {
      log(
        `👉  Get started with the following commands:\n\n` +
        (this.context === process.cwd() ? `` : chalk.cyan(` ${chalk.gray('$')} cd ${name}\n`)) +
        chalk.cyan(` ${chalk.gray('$')} ${packageManager === 'yarn' ? 'yarn serve' : packageManager === 'pnpm' ? 'pnpm run serve' : 'npm run serve'}`)
      )
    }
    log()

    if (gitCommitFailed) {
      warn(
        `Skipped git commit due to missing username and email in git config, or failed to sign commit.\n` +
        `You will need to perform the initial commit yourself.\n`
      )
    }

    generator.printExitLogs()

  }

  // { id: options } => [{ id, apply, options }]
  async resolvePlugins (rawPlugins, pkg) {
    // ensure cli-service is invoked first
    rawPlugins = sortObject(rawPlugins, ['@vue/cli-service'], true)
    const plugins = []

    // rawPlugins: {
    //   '@vue/cli-service': {
    //     projectName: 'aaa',
    //     vueVersion: '2',
    //     useConfigFiles: false,
    //     cssPreprocessor: undefined,
    //     plugins: { '@vue/cli-plugin-babel': {}, '@vue/cli-plugin-eslint': [Object] }
    //   },
    //   '@vue/cli-plugin-babel': {},
    //   '@vue/cli-plugin-eslint': { config: 'base', lintOn: [ 'save' ] }
    // }

    for (const id of Object.keys(rawPlugins)) {
      // @vue/cli-service/generator
      // @vue/cli-plugin-babel/generator
      // @vue/cli-plugin-eslint
      const apply = loadModule(`${id}/generator`, this.context) || (() => {})
      let options = rawPlugins[id] || {}

      if (options.prompts) {
        // 不执行
        let pluginPrompts = loadModule(`${id}/prompts`, this.context)

        if (pluginPrompts) {
          const prompt = inquirer.createPromptModule()

          if (typeof pluginPrompts === 'function') {
            pluginPrompts = pluginPrompts(pkg, prompt)
          }
          if (typeof pluginPrompts.getPrompts === 'function') {
            pluginPrompts = pluginPrompts.getPrompts(pkg, prompt)
          }

          log()
          log(`${chalk.cyan(options._isPreset ? `Preset options:` : id)}`)
          options = await prompt(pluginPrompts)
        }
      }

      plugins.push({ id, apply, options })
    }
    // plugins
    // plugins: [
    //   {
    //     id: '@vue/cli-service',
    //     apply: [Function],
    //     options: {
    //       projectName: 'aaa',
    //       vueVersion: '2',
    //       useConfigFiles: false,
    //       cssPreprocessor: undefined,
    //       plugins: [Object]
    //     }
    //   },
    //   { id: '@vue/cli-plugin-babel', apply: [Function], options: {} },
    //   {
    //     id: '@vue/cli-plugin-eslint',
    //     apply: [Function] { hooks: [Function], applyTS: [Function] },
    //     options: { config: 'base', lintOn: [Array] }
    //   }
    // ]
    return plugins
  }

  async promptAndResolvePreset (answers = null) {
    // prompt
    if (!answers) {
      answers = await inquirer.prompt(this.resolveFinalPrompts())
    }

    // console.log(answers);
    
    let preset
    if (answers.preset && answers.preset !== '__manual__') {
      preset = await this.resolvePreset(answers.preset)
    } else {
      // manual
      preset = {
        useConfigFiles: answers.useConfigFiles === 'files',
        plugins: {}
      }
      answers.features = answers.features || []
      // run cb registered by prompt modules to finalize the preset
      this.promptCompleteCbs.forEach(cb => cb(answers, preset))
    }

    // __manual__  save preset
    if (answers.save && answers.saveName && savePreset(answers.saveName, preset)) {
      log()
      log(`🎉  Preset ${chalk.yellow(answers.saveName)} saved in ${chalk.yellow(rcPath)}`)
    }

    return preset
  }

  async resolvePreset (name) { // Default (Vue 2)
    let preset
    const savedPresets = getPresets() // 'Default (Vue 3)': {...}, 'Default (Vue 2)': {...},

    if (name in savedPresets) {
      preset = savedPresets[name]
    }
    return preset // { vueVersion: '2', useConfigFiles, cssPreprocessor, plugins }
  }

  resolveFinalPrompts () {
    // patch generator-injected prompts to only show in manual mode
    this.injectedPrompts.forEach(prompt => {
      const originalWhen = prompt.when || (() => true)
      prompt.when = answers => {
        return isManualMode(answers) && originalWhen(answers)
      }
    })

    const prompts = [
      this.presetPrompt,
      this.featurePrompt,
      ...this.injectedPrompts,
      ...this.outroPrompts
    ]
    return prompts
  }

  // 自定义特性
  resolveOutroPrompts () {
    const outroPrompts = [
      {
        name: 'useConfigFiles',
        when: isManualMode,
        type: 'list',
        message: 'Where do you prefer placing config for Babel, ESLint, etc.?',
        choices: [
          {
            name: 'In dedicated config files',
            value: 'files'
          },
          {
            name: 'In package.json',
            value: 'pkg'
          }
        ]
      },
      {
        name: 'save',
        when: isManualMode,
        type: 'confirm',
        message: 'Save this as a preset for future projects?',
        default: false
      },
      {
        name: 'saveName',
        when: answers => answers.save,
        type: 'input',
        message: 'Save preset as:'
      }
    ]

    // ask for packageManager once
    const savedOptions = loadOptions()
    if (!savedOptions.packageManager && (hasYarn() || hasPnpm3OrLater())) {
      const packageManagerChoices = []

      if (hasYarn()) {
        packageManagerChoices.push({
          name: 'Use Yarn',
          value: 'yarn',
          short: 'Yarn'
        })
      }

      if (hasPnpm3OrLater()) {
        packageManagerChoices.push({
          name: 'Use PNPM',
          value: 'pnpm',
          short: 'PNPM'
        })
      }

      packageManagerChoices.push({
        name: 'Use NPM',
        value: 'npm',
        short: 'NPM'
      })

      outroPrompts.push({
        name: 'packageManager',
        type: 'list',
        message: 'Pick the package manager to use when installing dependencies:',
        choices: packageManagerChoices
      })
    }

    return outroPrompts
  }
}

module.exports = Creator